package aaa

import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import org.junit.jupiter.api.*
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import java.io.File
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.fail
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.RequestEntity
import org.springframework.http.ResponseEntity
import org.springframework.web.client.HttpClientErrorException


@Disabled
internal class TestAudit: TestBase(){
    // 🦜 : This class assumes nothing, test some raw logic for Dt perhaps
    private val log: Logger = LoggerFactory.getLogger(TestAudit::class.java)
    val user2uid = mapOf(1 to Dt.UserInfo2(uid = "1", name = "me"))
    val oven = AiAuditOven(user2uid)

    // @Timeout(5)                //5 seconds
    // @Test fun testHi(){
    //     val l = listOf(Dt.OneoffProcessInfo2(time = Date(), cmd = "ls", uid = 1, argv = "ls -l"))
    //     val r = oven.process(l)
    //     log.info("🦜: result of mock process: {}",  r[0].spsReason)
    //     assertEquals(r[0].spsScore, 0.7f)
    // }


    // @Timeout(20)                //5 seconds
    // @Test fun testProcessWithLamma(){
    //     val l = listOf(Dt.OneoffProcessInfo2(time = Date(), cmd = "ls", uid = 1, argv = "ls -l"))
    //     oven.running.set(true)
    //     val r = oven.processWithOllama(l)
    //     log.info("🦜: result of mock process: {}",  r[0].spsReason)
    //     assertTrue(r[0].spsScore >= 0.0f)
    // }


    @Timeout(40)                //5 seconds
    @Test fun testProcessWithQwen(){
        val l = listOf(Dt.OneoffProcessInfo2(time = Date(), cmd = "ls", uid = 1, argv = "ls -l"))
        oven.running.set(true)
        val r = oven.processWithQwen25(l)
        log.info("🦜: result of mock process: {}",  r[0].spsReason)
        assertTrue(r[0].spsScore >= 0.0f)
    }

    @Test fun testRemoveTags(){
        var o1 = "<think> yo </think>hi"
        if (o1.contains("<think>")){
            val i = o1.indexOf("<think>")
            val j = o1.indexOf("</think>")
            o1 = o1.substring(0,i) + o1.substring(j+8)
        }
        assertEquals(o1, "hi")
    }
}


@Disabled
internal class TestRawLogic: TestBase(){
    // 🦜 : This class assumes nothing, test some raw logic for Dt perhaps
    private val log: Logger = LoggerFactory.getLogger(TestRawLogic::class.java)
    @Test fun testToAuditJson(){
        val o = Dt.OneoffProcessInfo2(time = Date(), cmd = "bash", uid = 1, argv = "bash -c 'echo hi'")

        val m: Map<Int, Dt.UserInfo2> = mapOf(1 to Dt.UserInfo2(uid = "1", name = "me"))
        val n = o.toAuditJson(m)

        log.info("🦜: converted json n: {}",  n)
        assertEquals(n["user"].asText(), "me")
    }

    @Autowired lateinit var repo: OneoffProcessInfo2Repo

    @Test fun testDoIt(){
        // 0. clear the repo
        repo.deleteAll()
        // 1. add some records
        for (i in 1..10)
            repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "bash", uid = i))
        // 2. start the box
        val b = AiAuditOven(mapOf(1 to Dt.UserInfo2(uid = "1", name = "me")))
        b.doIt(repo)
        // 3. check the repo
        val l = repo.findAll()
        assertEquals(l[0].spsScore, 0.7f)
        for (i in l){
            log.info("🦜: processed i: id: {}, spsScore {}, spsReason: {}", i.id, i.spsScore, i.spsReason)
            assertTrue(i.spsReason.isNotEmpty())
        }
    }
}

@Disabled
internal class TestFileIORecords: TestBase(){
    // 🦜 : This class assumes that the agent is up, and will monitors /home/me/Desktop/'s activity
    private val log: Logger = LoggerFactory.getLogger(TestFileIORecords::class.java)
    @Autowired lateinit var cl: FsController

    @Test fun testHi(){
        // 0. clear the repo and the field
        val f = File("/home/me/Desktop/hi.txt")
        if (f.exists()) f.delete()
        cl.repo.deleteAll()

        // 0.1 start listening if it's not
        if (!cl.up.get()) cl.toggle()

        // 1. touch hi.txt
        f.createNewFile()
        // wait for the agent to pick up
        Thread.sleep(500)

        // 2. it should have been recorded
        var l = cl.repo.findAll()
        l = l.filter({it.fileName.endsWith("hi.txt")})
        assertEquals(l.size, 1)
        log.info("🦜: Found : {}",  l)
        val o = l.first()
        assertEquals("CREATE", o.eventType)

        // 3. clean up
        f.delete()
    }

    @Test fun testCompile(){
        // 0. stop streaming if so
        if (cl.up.get()) cl.toggle()
        // 0.1 clear the repo
        cl.repo.deleteAll()

        // 1. throw the records in
        val t = 3600L * 365 * 24 * 60 * 1000
        cl.repo.save(Dt.FileIORecord2(time = Date(t), eventType = "CREATE", fileName = "ho.txt"))
        cl.repo.save(Dt.FileIORecord2(time = Date(t), eventType = "RENAME", fileName = "hi.txt"))
        cl.repo.save(Dt.FileIORecord2(time = Date(t + 1), eventType = "RENAME", fileName = "wo.txt"))

        assertEquals(3, cl.repo.count())
        // 2. compile into : RENAME(from = hi.txt, to = hi.txt) + TRASH(wo.txt)
        cl.compile()

        // 3. check
        val l = cl.repo.findAll()
        assertEquals(2, l.size)
        var o = l.filter({it.eventType == "RENAME"}).first()
        assertEquals(o.fileName, "hi.txt")
        assertEquals(o.fileTo, "ho.txt")
    }

}


@Disabled
internal class Test1OffLogic : TestBase() {
    // 🦜 : this class doesn't agent to be up. But reg43 assumes that a chain is up
    private val log: Logger = LoggerFactory.getLogger(Test1OffLogic::class.java)
    @Autowired
    lateinit var cl: CmdController

    // @Disabled
    @Test fun testRegStep41(){
        // 0. make sure that we are starting from a clean slate
        if (cl.up.get()) cl.toggle()
        cl.repo.deleteAll()

        // 1. add some mocked data
        for (i in 1..10)
            cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), argv = "bash", uid = i))
        // also some noise
        for (i in 1..3)
            cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), argv = "bash", uid = i, hash="dirty"))

        assertEquals(cl.repo.count(), 13)

        // 2. get the to-be-registed map
        val m = cl.regStep41(4) //should have [4,4,2], groups
        assertEquals(m.size, 3)
        for ( (k,v) in m){
            log.info("🦜: k: {}, v: {}", k, v)
            if (k.endsWith("0.bin")) assertEquals(v.rs.size, 4)
            if (k.endsWith("1.bin")) assertEquals(v.rs.size, 4)
            if (k.endsWith("2.bin")) assertEquals(v.rs.size, 2)

            // 🦜 : the hashUri therein should have been properly set
            assertEquals(v.rs[0].hashUri, k)
        }
    }

    // @Disabled
    @Test fun testRegStep42(){
        // 0. make sure that we are starting from a clean slate
        if (cl.up.get()) cl.toggle()
        cl.repo.deleteAll()

        // 1. add some mocked data
        for (i in 1..10)
            cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), argv = "bash", uid = i))

        // 2. get the to-be-registed map
        val m = cl.regStep41(4) //should have [4,4,2], groups
        // 3. persist them
        // 3.1 clear the ./desktop/zero/ folder
        val fd = File("./desktop/zero/")
        assertTrue(fd.deleteRecursively())
        fd.mkdirs()
        // 3.2 persist
        cl.regStep42(m)
        // 4. check the ./desktop/zero/ folder
        // 4.1 get the files
        val fs = fd.listFiles()
        assertEquals(fs.size, 3)
    }

    @Test fun testRegStep43(){
        // 0. make sure that we are starting from a clean slate
        if (cl.up.get()) cl.toggle()
        cl.repo.deleteAll()
        // 1. add some mocked data
        for (i in 1..10)
            cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), argv = "bash", uid = i))

        // 2. get the to-be-registed map
        var m = cl.regStep41(4) //should have [4,4,2], groups
        m = cl.regStep42(m)
        m = cl.regStep43(m)
        // 3. now the txHash field of each record should have been set
        for ((k,v) in m){
            log.info("🦜: k: {}, v: {}", k, v)
            assertNotNull(v.rs[0].hash)
            assertNotNull(v.txHash)
        }
    }

    // @Disabled
    @Test fun testFoldHi(){
        // 0. turn off the streaming
        if (cl.up.get()) cl.toggle()

        // 1. delete all
        cl.repo.deleteAll()
        // 1.2 add some mock data
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2000,2,1,0,0,0)) , argv = "bash", uid = 1))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2010,2,1,0,0,0)) , argv = "bash", uid = 1))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2020,2,1,0,0,0)) , argv = "bash", uid = 1))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2030,2,1,0,0,0)) , argv = "bash", uid = 1))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), argv = "joke", uid = 2))

        assertEquals(cl.repo.count(), 5)
        // 2. fold/compile
        val n = cl.fold()
        assertEquals(n, 2)      //two compiled

        // 3. check
        // three remains
        assertEquals(cl.repo.count(), 3)
        // the nFolded is incremented
        val l = cl.repo.findAll()
        log.info("🦜: found compiled : {}",  l)
        for (p in l){
            log.info("🦜: p: {}, {}, {}", p.time,p.argv, p.id)
        }
        // assertEquals(l[1].nFolded, 2)
        // assertEquals(l[1].time, Dt.new2Old(LocalDateTime.of(2030,2,1,0,0,0))) // ❌️
        // assertEquals(0,l[1].time!!.compareTo(Dt.new2Old(LocalDateTime.of(2030,2,1,0,0,0)))) //✅️
        // 🦜 : be careful when comparing dates..
        var p = l.filter({
                             it.time!!.compareTo(Dt.new2Old(LocalDateTime.of(2030,2,1,0,0,0))) == 0
                         }).first()
        assertEquals(p.nFolded , 2)

    }

    // @Disabled
    @Test fun getPk() {
        log.info("🦜: cl.pk: {}",  cl.pk)
        Assertions.assertTrue(cl.pk.length > 0)
        // 🦜 : It should has three lines (trimmed)
        val l = cl.pk.trim().split("\n")
        log.info("�t: l: {}",  l)
        Assertions.assertEquals(3, l.size)

        // Assertions.assertTrue(false)
        Assertions.assertTrue(l[0].startsWith("-----BEGIN PUBLIC KEY-----"))
        Assertions.assertTrue(l[1].length > 0)
        Assertions.assertTrue(l[2].startsWith("-----END PUBLIC KEY-----"))
    }
}


internal object S {
    const val RED: String = "\u001B[31m"
    const val GREEN: String = "\u001B[32m"
    const val YELLOW: String = "\u001B[33m"
    const val BLUE: String = "\u001B[34m"
    const val MAGENTA: String = "\u001B[35m"
    const val CYAN: String = "\u001B[36m"
    const val NOR: String = "\u001B[0m"
}

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
internal abstract class TestBase {

    // 🦜 : above is the kotlin way, below is the java way...
    @Autowired
    lateinit var restTemplate: TestRestTemplate
    @Autowired
    lateinit var json: ObjectMapper

    @LocalServerPort
    protected var port: Int = 0

    protected fun url(): String {
        return "http://localhost:$port"
    }

    @Throws(JsonProcessingException::class)
    fun getJson(path: String): JsonNode {
        val req: RequestEntity<Void> = RequestEntity.get(url() + path).build()
        // restTemplate ?: throw RuntimeException("restTemplate is null")
        val r: ResponseEntity<String> = restTemplate.exchange<String>(req, String::class.java)
        log.info("r: {}",  r)
        // json ?: throw RuntimeException("json is null")
        assertTrue(r.getStatusCode().is2xxSuccessful())
        return json.readTree(r.getBody())
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(body: String, method: String) {
        val req: RequestEntity<String> = RequestEntity.post(url() + method)
            .header("Content-Type", "application/json")
            .body<String>(body)

        // restTemplate ?: throw RuntimeException("restTemplate is null")
        val r: ResponseEntity<String> = restTemplate.exchange<String>(req, String::class.java)
        log.info("result for {}: {}", method,  r)
        Assertions.assertTrue(r.getStatusCode().is2xxSuccessful())
        Assertions.assertEquals(OK, r.getBody())
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(node: ArrayNode, method: String) {
        // json ?: throw RuntimeException("json is null")
        val json_s: String = json.writeValueAsString(node)
        assertPostOk(json_s, method)
    }

    @Throws(JsonProcessingException::class)
    fun assertPostOk(node: ObjectNode, method: String) {
        val json_s: String = json.writeValueAsString(node)
        assertPostOk(json_s, method)
    }

    companion object {
        val log: Logger = LoggerFactory.getLogger(TestBase::class.java)
        fun old2New(d: Date): LocalDateTime {
            return LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())
        }

        fun new2Old(l: LocalDateTime): Date {
            return Date.from(l.atZone(ZoneId.systemDefault()).toInstant())
        }

        const val OK: String = "{\"code\":0,\"msg\":\"Success\"}"
    }
}


@Disabled
internal class TestChain: TestBase(){
    private val log: Logger = LoggerFactory.getLogger(TestChain::class.java)

    @Autowired
    lateinit var cn: ChainController

    @Disabled
    @Test
    fun ping(){
        Assertions.assertTrue(cn.ping())
    }

    @Test
    fun nodeStatus(){
        val nodeStatus = cn.nodeStatus()
        log.info("nodeStatus: {}",  nodeStatus)
        assertNotNull(nodeStatus)
    }
}


@Disabled
internal class Test1OffStop : TestBase() {
    // 🦜 : 📗️ : this test unit assumes that the agent is up and streams event at 1 rec/3sec
    private val log: Logger = LoggerFactory.getLogger(Test1OffStop::class.java)

    @Autowired
    lateinit var cl: CmdController 

    // @Disabled
    @Test
    fun getNormal() {
        // 0. make sure that we are starting from a clean slate
        if (!cl.up.get()) {
            cl.toggle()
        }

        // 1. count
        val n = cl.repo.count()

        // 2. wait
        Thread.sleep(5000)      //🦜 : by default, the mock agent emits 1 rec/ 3sec

        // 3. get again
        val n1 = cl.repo.count()
        // 4. compare
        log.info("🦜: n: {}, n1: {}",  n, n1)
        Assertions.assertTrue(n1 > n)
    }

    // @Disabled
    @Test
    fun getStop() {
        // 1. count
        val n = cl.repo.count()
        var r = cl.isUp()
        log.info("🦜: cl.isUp() : {}",  r)
        Assertions.assertTrue(r.data)
        r = cl.toggle()
        log.info("🦜: t:cl.toggle() r: {}",  r)
        Assertions.assertTrue(!r.data)

        // 2. wait
        Thread.sleep(10000)      //🦜 : by default, the mock agent emits 1 rec/ 3sec

        // 3. get again
        val n1 = cl.repo.count()
        // 4. compare
        log.info("🦜: n: {}, n1: {}",  n, n1)
        Assertions.assertTrue(n1 <= n + 1)
        // 🦜 : okay.. it seems like it will offshot by one, not that quite exact...
        r = cl.isUp()
        log.info("🦜 : cl.isUp() r: {}",  r)
        Assertions.assertTrue(!r.data)
    }
}

@Disabled
internal class Test1OffDb : TestBase() {
    // 🦜 : 📗️ : this test unit assumes that the agent is down...
    private val log: Logger = LoggerFactory.getLogger(Test1OffDb::class.java)

    @Autowired
    lateinit var cl: CmdController 

    @Test
    fun getHi() {
        cl.repo.deleteAll()

        var pageNode = getJson("/cmd/get?pageNum=1&pageSize=2")
        log.info("pageNode: {}",  pageNode)
        pageNode = pageNode["data"]
        val voList = pageNode["voList"] as ArrayNode
        Assertions.assertEquals(0, voList.size())
    }

    // class OneoffProcessInfo2(
    //     @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0,
    //     var time: Date? = null,
    //     var cmd: String = "",
    //     var pid: Int = -1,
    //     var ppid: Int = -1,
    //     var uid: Int = -1,
    //     @Column(length = 512) var argv: String = "",  // this is capped to 128 at the agent side. (the C code)
    //     var retVal: Int = 0,
    // )

    @Test
    fun filterCmd() {
        cl.repo.deleteAll()
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "bash"))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "josh"))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "joke"))

        var pageNode = getJson("/cmd/get?pageNum=1&pageSize=2&cmd=sh")
        log.info("pageNode: {}",  pageNode)
        pageNode = pageNode["data"]
        val voList = pageNode["voList"] as ArrayNode
        Assertions.assertEquals(2, voList.size())
    }

    @Test
    fun filterUid() {
        cl.repo.deleteAll()
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "bash", uid=4))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "josh", uid=3))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Date(), cmd = "joke", uid=3))

        var pageNode = getJson("/cmd/get?pageNum=1&pageSize=2&uid=4")
        log.info("pageNode: {}",  pageNode)
        pageNode = pageNode["data"]
        val voList = pageNode["voList"] as ArrayNode
        Assertions.assertEquals(1, voList.size())
    }


    @Test
    fun filterTime() {
        cl.repo.deleteAll()
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2000,2,1,0,0,0)), cmd = "bash", uid=4))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2010,2,1,0,0,0)), cmd = "josh", uid=4))
        cl.repo.save(Dt.OneoffProcessInfo2(time = Dt.new2Old(LocalDateTime.of(2020,2,1,0,0,0)), cmd = "joke", uid=4))

        var pageNode = getJson("/cmd/get?pageNum=1&pageSize=2&sinceTime=2010-01-01 00:00:00")
        var voList = pageNode["data"]["voList"] as ArrayNode
        Assertions.assertEquals(2, voList.size())

        // after
        pageNode = getJson("/cmd/get?pageNum=1&pageSize=2&untilTime=2010-01-01 00:00:00")
        voList = pageNode["data"]["voList"] as ArrayNode
        Assertions.assertEquals(1, voList.size())

        // between
        pageNode = getJson("/cmd/get?pageNum=1&pageSize=2&untilTime=2010-05-01 00:00:00&sinceTime=2005-01-01 00:00:00")
        voList = pageNode["data"]["voList"] as ArrayNode
        Assertions.assertEquals(1, voList.size())
    }



} // class TestOld2


